/**
 * 
 */
package cz.cuni.mff.abacs.burglar.visual.play_state;

import java.util.List;

import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;

import cz.cuni.mff.abacs.burglar.logics.DataMap;
import cz.cuni.mff.abacs.burglar.logics.ExecutingMap;
import cz.cuni.mff.abacs.burglar.logics.GameMap;
import cz.cuni.mff.abacs.burglar.logics.GameMap.StepResult;
import cz.cuni.mff.abacs.burglar.logics.objects.BaseObject;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Active;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Lockable;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Vender;
import cz.cuni.mff.abacs.burglar.logics.planning.instructions.Instruction;
import cz.cuni.mff.abacs.burglar.logics.storage.XMLSaver;
import cz.cuni.mff.abacs.burglar.visual.Main;
import cz.cuni.mff.abacs.burglar.visual.multithreading.LoadingListener;
import cz.cuni.mff.abacs.burglar.visual.multithreading.LoadingThread;
import cz.cuni.mff.abacs.burglar.visual.multithreading.PlanningListener;
import cz.cuni.mff.abacs.burglar.visual.multithreading.PlanningThread;
import cz.cuni.mff.abacs.burglar.visual.multithreading.PlanningThread.Type;


/**
 * Base of the gameplay implementation. It contains the game logics.
 * 
 * @author abacs
 *
 */
public abstract class PlayState 
		extends BasicGameState 
		implements PlanningListener, LoadingListener {

	// constants:
	
	/** Time spent in milliseconds between two movement steps. */
	protected static int DEFAULT_MOVEMENT_DELAY = 600;
	/** Time change in milliseconds when game speed is lowered or increased. */
	protected static int GAME_SPEED_DELTA = 200;
	
	protected static final String DEFAULT_MAP = null;
	
	/**  */
	public static enum mapState {
		UNSET,
		UNLOADED,
		SELECING_TRAP_ROOMS,
		UNPLANED,
		PLANNING,
		PLANNED,
		UNSOLVABLE,
		SOLVED,
		LOST,
	};
	
	/**  */
	public static enum runingState {
		PAUSED,
		MOVING,
	}
	
	
	// -------------------------------------------------------------------------
	// properties:
	
	
	private final int _stateID;
	
	
	/** Pointer to the main game container */
	protected GameContainer _container = null;
	/**  */
	protected StateBasedGame _stateBasedGame = null;
	
	
	/**  */
	protected mapState _mapState = mapState.UNSET;
	/**  */
	protected runingState  _runingState  = runingState.MOVING;
	
	
	/** The loaded map file */
	protected GameMap _map = null;
	/**  */
	protected GameMap _secondaryMap = null;
	
	/** Name of the map file to execute */
	protected String _mapName = null;
	/** The thread executing the planning */
	protected PlanningThread _planningThread = null;	
	
	
	/** selection's position on the game map */
	protected Selection _selection = null;
	/**  */
	protected Screen _screen = null;
	
	
	/** milliseconds to wait between two game moves */
	protected int _movementDelay = DEFAULT_MOVEMENT_DELAY;
	/** milliseconds to wait until the next game move */
	protected int _msToWait = 0;
	
	
	
	// TODO setters
	//public int _trapsToSet = 1;
	
	public int _trapsAvioded = 0;
	
	
	
	
	// -------------------------------------------------------------------------
	// constructor:
	
	
	/**
	 * 
	 * @param stateID
	 */
	protected PlayState(int stateID) 
			throws SlickException {	
		this._stateID = stateID;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	@Override
	public int getID() {
		return this._stateID;
	}
	

	/** 
	 * Initialize any needed data before the game loop.
	 * Warning: it may be called multiple times:
	 * 
	 * @param container
	 * @param game
	 * */
	@Override
	public void init(GameContainer container, StateBasedGame game)
			throws SlickException {
		
		this._container = container;
		this._stateBasedGame = game;

		this._mapName = DEFAULT_MAP;
		
		// load the map:
//		if(this._mapState == mapState.UNSET && this._map == null && this._mapName != null){
//			this.reload();
//		}
		
		// view settings:
		this._selection = new Selection(this);
		this._screen = new Screen(
				this,
				container.getWidth()  / Main.RESOURCE_BLOCK_SIZE,
				container.getHeight() / Main.RESOURCE_BLOCK_SIZE
		);
	}
	
	
	/**
	 * Called during the game to update the logic in our world,
	 * within this method we obtain the user input,
	 * calculate the world response to the input, do the calculation.
	 * 
	 * @param container
	 * @param game
	 * @param delta
	 * */
	@Override
	public void update(GameContainer container, StateBasedGame game, int delta)
			throws SlickException {
		
		// do the planning if needed:
		if(this._mapState == mapState.UNPLANED){
			
			this._mapState = mapState.PLANNING;
			this._planningThread = 
				new PlanningThread(
						this._map,
						Type.PLANNING,
						this
				);
			this._planningThread.start();
		}
		
		this._msToWait -= delta;
		
		
		// if the game is not paused - redraw:
		if(
			this._runingState == runingState.MOVING &&
			this._mapState == mapState.PLANNED &&
			this._msToWait <= 0
		){
			
			GameMap.StepResult stepResult = this._map.executeStep();		
			
			// reset delay:
			this._msToWait = this._movementDelay;
			
			if(this._map.getBurglar().isSatisfied()){
				this._mapState = mapState.SOLVED;
				System.out.println("- Solved");
				return;
			}
			
			if(this._map.isBurglarTrapped()){
				this._mapState = mapState.LOST;
				return;
			}
			
			if(stepResult == StepResult.IMPORTANT_CHANGE){
				this._mapState = mapState.UNPLANED;
				System.out.println("- important internal change, replanning");
				return;
			}
			
		}
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * shortcuts:
	 * - directions: moves the selection-box
	 * - exit: quits the program
	 * - space bar: pauses the game
	 * - + increase game speed
	 * - - decrease game speed
	 * 
	 * @param key
	 * @param c
	 */
	@Override
	public void keyReleased(int key, char c) {
		// 
		switch(key){
		case Input.KEY_UP:
			this._screen.moveUp();
			return;
		case Input.KEY_DOWN:
			this._screen.moveDown();
			return;
		case Input.KEY_LEFT:
			this._screen.moveLeft();
			return;
		case Input.KEY_RIGHT:
			this._screen.moveRight();
			return;
		case Input.KEY_SPACE:
			this.pauseUnpause();
			return;
		case Input.KEY_ADD:
			this.increaseSpeed();
			return;
		case Input.KEY_SUBTRACT:
			this.decreaseSpeed();
			return;
		case Input.KEY_ESCAPE:
			this._container.exit();
			return;
		default:
		}
	}
	
	
	@Override
	public void mouseMoved(int oldX, int oldY, int newX, int newY){
		super.mouseMoved(oldX, oldY, newX, newY);
		
		int mapX = this._screen.screenXToMapX(newX);
		int mapY = this._screen.screenYToMapY(newY);
		
		this._selection.set(mapX, mapY);
	}
	
	
	@Override 
	public void mouseClicked(int button, int x, int y, int clickCount) {
		
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**  */
	public void pauseUnpause() {
		
		// TODO TEMPORAL saving test
		XMLSaver.save((DataMap) this._map, "saved_map.xml");
		
		
		switch(this._runingState){
		case PAUSED:
			this._runingState = runingState.MOVING;
			break;
		case MOVING:
			this._runingState = runingState.PAUSED;
			break;
		default:
			break;
		}
	}
	
	
	public boolean increaseSpeed() {
		if(this._movementDelay > 0){
			this._movementDelay -= GAME_SPEED_DELTA;
			return true;
		}
		return false;
	}
	
	
	public boolean decreaseSpeed() {
		this._movementDelay += GAME_SPEED_DELTA;
		return true;
	}
	
	
	/** 
	 * Reloads the current map and start planning.
	 *  */
	public void reload() {
		// load the map:
		this._mapState = mapState.UNLOADED;
		this._map = null;
		this._secondaryMap = null;
		LoadingThread loader = new LoadingThread(this._mapName, this);
		loader.start();
	}
	
	
	/** 
	 * Loads a new map and start planning
	 *  */
	public void load(String mapName) {
		if(this._mapName != null && this._mapName.equals(mapName))
			return;
		this._mapName = mapName;
		this.reload();
	}
	
	
	/**
	 * 
	 */
	public void openCloseSelection() {
		boolean changed = false;
		List<BaseObject> objects = 
			this._map.getObjectsOnPosition(
					this._selection.getX(),
					this._selection.getY()
			);
		for(BaseObject object : objects){
			switch(object.getType()){
			case DOOR:
			case CONTAINER:
				Lockable lockable = (Lockable)object;
				if(lockable.isClosed())
					lockable.open();
				else
					lockable.close();
				changed = true;
				break;
			}
		}
		
		this.replanIfNeeded(changed);
	}
	
	
	/**
	 * 
	 */
	public void lockUnlockSelection() {
		boolean changed = false;
		List<BaseObject> objects = 
			this._map.getObjectsOnPosition(
					this._selection.getX(),
					this._selection.getY()
			);
		for(BaseObject object : objects){
			switch(object.getType()){
			case DOOR:
			case CONTAINER:
				Lockable lockable = (Lockable)object;
				if(lockable.isClosed() == false)
					continue;
				if(lockable.isLocked())
					lockable.unlock(lockable.getKeyId());
				else
					lockable.lock(lockable.getKeyId());
				changed = true;
				break;
			}
		}
		
		this.replanIfNeeded(changed);
	}
	
	
	/**
	 * 
	 */
	public void activateDeactivateSelection() {
		boolean changed = false;
		List<BaseObject> objects = 
			this._map.getObjectsOnPosition(
					this._selection.getX(),
					this._selection.getY()
			);
		for(BaseObject object : objects){
			switch(object.getType()){
			case CAMERA:
			case SWITCH:
			case PHONE:
				Active active = (Active)object;
				active.changeActiveState();
				changed = true;
				break;
			case VENDER:
				Vender vender = (Vender)object;
				if(vender.hasDropped() == false)
					vender.activate();
				changed = true;
				break;
			}
		}
		
		this.replanIfNeeded(changed);
	}
	
	
	/**
	 * If there was any relevant change, the agents should know about it, 
	 * and the game is in a state when planning is possible redo the planning.
	 * 
	 * @param anythingChanged
	 * @return 
	 */
	protected boolean replanIfNeeded(boolean anythingChanged) {
		if(
			Main.FLAG_ALL_KNOWING_AGENTS && anythingChanged &&
			(this._mapState == mapState.PLANNING ||
			 this._mapState == mapState.PLANNED ||
			 this._mapState == mapState.UNSOLVABLE)
		){
			System.out.println("- important external change, replanning");
			this._mapState = PlayState.mapState.UNPLANED;
			return true;
		}
		
		return false;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * 
	 */
	protected void synchronizeSecondaryMap() {
		// TODO
	}
	
	
	
	// -------------------------------------------------------------------------
	// callbacks:
	
	
	@Override
	public void planningFinished(
			List<Instruction> resultedInstructions,
			PlanningThread thread
	) {
		// do nothing, if not the most recent planning
		if(this._planningThread != thread)
			return;
		
		this._map.addInstructions(resultedInstructions);
		
	}
	
	
	@Override
	public void planningFinished(
			List<Instruction> resultedInstructions,
			List<Integer> avoidedTrapRooms,
			PlanningThread thread
	) {
		// do nothing, if not the most recent planning
		if(this._planningThread != thread)
			return;
		
		this._map.addInstructions(resultedInstructions);
		
		this._trapsAvioded = avoidedTrapRooms.size();
		
		if(resultedInstructions.size() > 0)
			this._mapState = mapState.PLANNED;
		else
			this._mapState = mapState.UNSOLVABLE;
	}
	
	
	@Override
	public void loadingFinished(ExecutingMap map, ExecutingMap secondaryMap) {
		this._map = map;
		this._secondaryMap = secondaryMap;
		
		if(Main.FLAG_IN_GAME_LEVEL_DESIGN){
			// start placing guards on the map:
			this._mapState = mapState.SELECING_TRAP_ROOMS;
			this._planningThread = 
					new PlanningThread(
							this._map,
							Type.SELECTING_TRAP_ROOMS,
							this
							);
			this._planningThread.start();
		}else{
			this._mapState = mapState.UNPLANED;
		}
		
	}
	
	
	@Override
	public void selectingTrapRoomsFinished(
			List<Integer> trapRooms,
			PlanningThread thread
	) {
		if(thread != this._planningThread)
			return;
		
		DataMap map = (DataMap)this._map;
		
		List<List<Integer>> components = map.breakToComponents(trapRooms);
		
		for(List<Integer> component : components){
			if(component.size() > 1){
				((ExecutingMap)map).addGuardPatrol(component);
			}else{
				map.addCameraToRoom(component.get(0));
			}
		}
		
		
		// map is ready for planning:
		this._mapState = mapState.UNPLANED;
	}
	
	
	// =========================================================================
	// subclasses:
	
	
	/**
	 * Representation of the mouse cursor's position on the screen.
	 */
	protected class Selection {
	
		// properties:
		
		/** selection's horizontal position on the game map */
		protected int _x = 0;
		/** selection's vertical position on the game map */
		protected int _y = 0;
		/**  */
		protected PlayState _parent = null;
		
		
		// ---------------------------------------------------------------------
		// constructors:
		
		
		/**
		 * 
		 * 
		 * @param parent
		 */
		protected Selection(PlayState parent) {
			this._parent = parent;
		}
		
		
		// ---------------------------------------------------------------------
		
		
		/**
		 * Allows the selection to move to the left on the map.
		 * 
		 * If the selection reaches the left edge of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveLeft() {
			if(this._parent._map == null)
				return false;
			
			if(this._x > 1){
				this._x--;
				return true;
			}
			return false;
		}

		
		/**
		 * Allows the selection to move to the right left the map.
		 * 
		 * If the selection reaches the right edge of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveRight() {
			if(this._parent._map == null)
				return false;
			
			if(this._x < this._parent._map.getWidth() + 1){
				this._x++;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Allows the selection to move up to the top of the map.
		 * 
		 * If the selection reaches the top of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveUp() {
			if(this._parent._map == null)
				return false;
			
			if(this._y > 1){
				this._y--;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Allows the selection to move down to the bottom of the map.
		 * 
		 * If the selection reaches the bottom of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveDown() {
			if(this._parent._map == null)
				return false;
			
			if(this._y < this._parent._map.getHeight() + 1){
				this._y++;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Sets the position of the selection on the map.
		 * 
		 * @param val x coordinate on the map.
		 * @return success indicator
		 */
		public boolean setX(int val) {
			if(this._parent._map == null)
				return false;
			
			if(val > 0 && val < this._parent._map.getWidth() + 1){
				this._x = val;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Sets the position of the selection on the map.
		 * 
		 * @param val y coordinate on the map.
		 * @return success indicator
		 */
		public boolean setY(int val) {
			if(this._parent._map == null)
				return false;
			
			if(val > 0 && val < this._parent._map.getHeight() + 1){
				this._y = val;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Sets the position of the selection on the map.
		 * 
		 * @param x x coordinate on the map.
		 * @param y y coordinate on the map.
		 * @return success indicator
		 */
		public boolean set(int x, int y) {
			if(this._parent._map == null)
				return false;
				
			if(x > 0 && x < this._parent._map.getWidth() + 1 &&
			   y > 0 && y < this._parent._map.getHeight() + 1){
				this._x = x;
				this._y = y;
				return true;
			}
			return false;
		}
		
		
		// ---------------------------------------------------------------------
		
		
		/** selection's horizontal position on the game map */
		public int getX() {
			return this._x;
		}
		
		
		/** selection's vertical position on the game map */
		public int getY() {
			return this._y;
		}
		
	}
	
	
	/**
	 * Representation of the visible part of the GameMap.
	 */
	protected class Screen {
	
		// properties:
		
		/** Screen's left corner on the game map */
		protected int _x = 0;
		/** Screen's top corner on the game map */
		protected int _y = 0;
		/** screen's width in game map positions */
		protected int _width = 0;
		/** screen's height in game map positions */
		protected int _height = 0;
		/**  */
		protected PlayState _parent = null;
		
		
		// ---------------------------------------------------------------------
		// constructors:
		
		
		/**
		 * 
		 * 
		 * @param parent
		 * @param width
		 * @param height
		 */
		protected Screen(PlayState parent, int width, int height) {
			this._parent = parent;
			this._width  = width;
			this._height = height;
		}
		
		
		// ---------------------------------------------------------------------
		
		
		/**
		 * Allows to move left to show the most right and one unused 
		 * wall position.
		 * 
		 * If the screen reaches the left border of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveLeft() {
			if(this._x > 0){
				this._x--;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Allows to move right to show the most right and one unused
		 * wall position.
		 * 
		 * If the screen reaches the right border of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveRight() {
			if(this._x + this._width < this._parent._map.getWidth() + 2){
				this._x++;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Allows to move up to show the highest and one unused wall position.
		 * 
		 * If the screen reaches the top of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveUp() {
			if(this._y > 0){
				this._y--;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Allows to move down to show the lowest and one unused wall position.
		 * 
		 * If the screen reaches the bottom of the map, does nothing.
		 * 
		 * @return success indicator
		 */
		public boolean moveDown() {
			if(this._y + this._height < this._parent._map.getHeight() + 2){
				this._y++;
				return true;
			}
			return false;
		}
		
		
		/**
		 * Sets the width of the game map.
		 * 
		 * If the value is invalid (less than 1), does nothing, prints to error
		 * output.
		 * 
		 * @param value new width.
		 */
		public void setWidth(int value) {
			if(value < 1){
				System.err.println(value + " is not a valid game map width.");
				return;
			}
			this._width = value;
		}
		
		
		/**
		 * Sets the height of the game map.
		 * 
		 * If the value is invalid (less than 1), does nothing, prints to error
		 * output.
		 * 
		 * @param value new height.
		 */
		public void setHeight(int value) {
			if(value < 1){
				System.err.println(value + " is not a valid game map height.");
				return;
			}
			this._height = value;
		}
		
		
		// ---------------------------------------------------------------------
		
		
		/** Screen's left corner on the game map */
		public int getX() {
			return this._x;
		}
		
		
		/** Screen's top corner on the game map */
		public int getY() {
			return this._y;
		}
		
		
		/** screen's width in game map positions */
		public int getWidth() {
			return this._width;
		}
		
		
		/** screen's height in game map positions */
		public int getHeight() {
			return this._height;
		}
		
		
		// ---------------------------------------------------------------------
		
		
		/**
		 * Converts the screen position to the map position.
		 * 
		 * @param screenX x coordinate on the screen
		 * @return x coordinate on the map
		 */
		public int screenXToMapX(int screenX) {
			return screenX / Main.RESOURCE_BLOCK_SIZE + this._x;
		}
		
		
		/**
		 * Converts the screen position to the map position.
		 * 
		 * @param screenX x coordinate on the screen
		 * @return x coordinate on the map
		 */
		public int screenXToMapX(float screenX) {
			return this.screenXToMapX((int)screenX);
		}
		
		
		/**
		 * Converts the screen position to the map position.
		 * 
		 * @param screenY y coordinate on the screen
		 * @return y coordinate on the map
		 */
		public int screenYToMapY(int screenY) {
			return screenY / Main.RESOURCE_BLOCK_SIZE + this._y;
		}
		
		
		/**
		 * Converts the screen position to the map position.
		 * 
		 * @param screenY y coordinate on the screen
		 * @return y coordinate on the map
		 */
		public int screenYToMapY(float screenY) {
			return this.screenYToMapY((int)screenY);
		}
		
		
		/**
		 * Converts the map position to the screen position.
		 * 
		 * @param mapX x coordinate on the map
		 * @return x coordinate on the screen
		 */
		public int mapXToScreenX(int mapX) {
			return (mapX - this._x) * Main.RESOURCE_BLOCK_SIZE;
		}
		
		
		/**
		 * Converts the map position to the screen position.
		 * 
		 * @param mapY y coordinate on the map
		 * @return y coordinate on the screen
		 */
		public int mapYToScreenY(int mapY) {
			return (mapY - this._y) * Main.RESOURCE_BLOCK_SIZE;
		}
		
	}

}
